package com.tymoshenko.seabattle.server.shipplacement;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.tymoshenko.seabattle.client.rpc.shipplacement.ShipPlacementService;
import com.tymoshenko.seabattle.shared.model.domain.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * @author Yakiv Tymoshenko
 * @since 07.02.13
 */
public class ShipPlacementServiceImpl extends RemoteServiceServlet implements ShipPlacementService {

    private static final Random random = new Random();

    public Player[] placeShips(Account account1, Account account2) {
        Player[] players = new Player[2];

        players[0] = createPlayerAndPlaceHisShips();
        players[1] = createPlayerAndPlaceHisShips();

        players[0].setAccount(account1);
        players[1].setAccount(account2);

        return players;
    }

    /*
       Place exact number of each Ship.Type defined in Ship.Rules
    */
    private Player createPlayerAndPlaceHisShips() {
        Player player = new Player();
        Board board = new Board();
        Fleet fleet = new Fleet();

        for (Map.Entry<Ship.Type, Integer> typeIntegerEntry : Ship.getPlacementRules().entrySet()) {
            for (int i = 0; i < typeIntegerEntry.getValue(); i++) {
                Ship ship = tryPlaceRandomShip(typeIntegerEntry.getKey(), board);
                drawShip(board, ship);
                fleet.addShip(ship);
            }
        }

        player.setBoard(board);
        player.setFleet(fleet);
        return player;
    }

    /*
   Check:
       1) ship coordinates within the board;
       2) ships do not touch each other;
       3) ships do not overlap.
    */
    // TODO introduce perebor like 8queens task
    private Ship tryPlaceRandomShip(Ship.Type type, Board board) {
        Ship ship;
        int tries = 0;      // to prevent eternal loops

        do {
            Ship.Direction direction = randomDirection();
            List<Point> firstDeckCandidates = firstDeckCandidates(board, type, direction);
            if (firstDeckCandidates.size() == 0) {
                direction = flip(direction);
                firstDeckCandidates = firstDeckCandidates(board, type, direction);
                if (firstDeckCandidates.size() == 0) {
                    ship = null;     // should never reach here
                    continue;
                }
            }
            ship = new Ship(type, direction, randomPoint(firstDeckCandidates));
            tries++;
        } while (!(doesNotOverlapWithOtherShips(ship, board) && doesNotTouchOtherShips(ship, board)
                && allDecksWithinTheBoard(ship)) && tries < 50);

        return ship;
    }

    private void drawShip(Board board, Ship ship) {
        for (Point deck : ship.getDecks()) {
            board.setFieldStatus(deck, Board.Status.DECK);
        }
        for (Point border : Board.shipNeighbourFields(ship)) {
            board.setFieldStatus(border, Board.Status.BORDER);
        }
    }

    private Point randomPoint(List<Point> pointList) {
        return pointList.get(random.nextInt(pointList.size()));
    }

    private Ship.Direction randomDirection() {
        if (random.nextBoolean()) {
            return Ship.Direction.VERTICAL;
        } else {
            return Ship.Direction.HORIZONTAL;
        }
    }

    private List<Point> firstDeckCandidates(Board board, Ship.Type type, Ship.Direction direction) {
        List<Point> result = new ArrayList<Point>();
        for (Point field : freeFields(board)) {
            // ensure last deck is within the board
            if (direction == Ship.Direction.VERTICAL) {
                if (Board.HEIGHT - field.getRow() + 1 >= type.getDecks()) {
                    result.add(field);
                }
            } else {
                if (Board.WIDTH - field.getCol() + 1 >= type.getDecks()) {
                    result.add(field);
                }
            }
        }
        return result;
    }

    private List<Point> freeFields(Board board) {
        List<Point> result = new ArrayList<Point>(Board.WIDTH * Board.HEIGHT);
        for (Map.Entry<Point, Board.Status> field : board.getFields().entrySet()) {
            if (field.getValue() == Board.Status.FREE) {
                result.add(field.getKey());
            }
        }
        return result;
    }

    private boolean isWithinTheBoard(Point point) {
        return (point.getRow() >= 1 && point.getRow() <= Board.HEIGHT) &&
                (point.getCol() >= 1 && point.getCol() <= Board.WIDTH);
    }

    // Not actually needed, because within-the-board is checked by Point.valueOf(int, int)
    private boolean allDecksWithinTheBoard(Ship ship) {
        Point firstDeck = ship.getFirstDeck();
        Point lastDeck = ship.getLastDeck();
        return isWithinTheBoard(firstDeck) && isWithinTheBoard(lastDeck);
    }

    private boolean doesNotTouchOtherShips(Ship ship, Board board) {
        for (Point neighbour : Board.shipNeighbourFields(ship)) {
            if (board.getFields().get(neighbour) == Board.Status.DECK) {
                return false;
            }
        }
        return true;
    }

    private boolean doesNotOverlapWithOtherShips(Ship ship, Board board) {
        return freeFields(board).containsAll(ship.getDecks());
    }

    private Ship.Direction flip(Ship.Direction oldDirection) {
        if (oldDirection == Ship.Direction.VERTICAL) {
            return Ship.Direction.HORIZONTAL;
        } else {
            return Ship.Direction.VERTICAL;
        }
    }
}
